--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 3a3b7de10c9d5e5c2d0eb77f1b269299d0f1b46e
Parents : 5e66dc0
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-12T17:41:00-05:00
feat(conversation): improve ConversationDropDownMenu and ConversationViewer with responsive actions and improved GlobalState handling
Changes
3 files changed, 94 insertions(+), 18 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue b/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue
index 6bcf411b..4524b1ec 100644
--- a/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue
@@ -1,5 +1,5 @@
<template>
- <DropDownMenu>
+ <DropDownMenu v-if="compact">
<template #button>
<IconButton>
<MaterialDesignIcon icon-name="dots-vertical" class="size-7" />
@@ -64,7 +64,7 @@
</div>
<!-- telemetry trust toggle -->
- <div v-if="GlobalState.config?.telemetry_enabled" class="border-t">
+ <div v-if="GlobalState?.config?.telemetry_enabled" class="border-t">
<DropDownMenuItem @click="onToggleTelemetryTrust">
<MaterialDesignIcon
:icon-name="contact?.is_telemetry_trusted ? 'shield-check' : 'shield-outline'"
@@ -80,6 +80,55 @@
</div>
</template>
</DropDownMenu>
+ <div v-else class="flex items-center gap-0.5 sm:gap-1 flex-wrap justify-end max-w-[min(100%,52vw)] sm:max-w-none">
+ <IconButton
+ v-if="hasFailedMessages"
+ :title="$t('messages.retry_failed')"
+ class="shrink-0"
+ @click="$emit('retry-failed')"
+ >
+ <MaterialDesignIcon icon-name="refresh" class="size-5 text-red-500" />
+ </IconButton>
+ <IconButton :title="$t('messages.telemetry_history')" class="shrink-0" @click="$emit('open-telemetry-history')">
+ <MaterialDesignIcon icon-name="satellite-variant" class="size-5" />
+ </IconButton>
+ <IconButton :title="$t('messages.start_call')" class="shrink-0" @click="$emit('start-call')">
+ <MaterialDesignIcon icon-name="phone" class="size-5" />
+ </IconButton>
+ <IconButton :title="$t('messages.share_contact')" class="shrink-0" @click="$emit('share-contact')">
+ <MaterialDesignIcon icon-name="notebook-outline" class="size-5" />
+ </IconButton>
+ <IconButton :title="$t('messages.pop_out_chat')" class="shrink-0" @click="$emit('popout')">
+ <MaterialDesignIcon icon-name="open-in-new" class="size-5" />
+ </IconButton>
+ <IconButton title="Ping Destination" class="shrink-0" @click="onPingDestination">
+ <MaterialDesignIcon icon-name="flash" class="size-5" />
+ </IconButton>
+ <IconButton :title="$t('messages.custom_display_name')" class="shrink-0" @click="onSetCustomDisplayName">
+ <MaterialDesignIcon icon-name="account-edit" class="size-5" />
+ </IconButton>
+ <IconButton v-if="!isBlocked" title="Banish User" class="shrink-0" @click="onBlockDestination">
+ <MaterialDesignIcon icon-name="gavel" class="size-5 text-red-500" />
+ </IconButton>
+ <IconButton v-else title="Lift Banishment" class="shrink-0" @click="onUnblockDestination">
+ <MaterialDesignIcon icon-name="check-circle" class="size-5 text-green-500" />
+ </IconButton>
+ <IconButton title="Delete Message History" class="shrink-0" @click="onDeleteMessageHistory">
+ <MaterialDesignIcon icon-name="delete" class="size-5 text-red-500" />
+ </IconButton>
+ <IconButton
+ v-if="GlobalState?.config?.telemetry_enabled"
+ :title="contact?.is_telemetry_trusted ? $t('app.telemetry_trust_revoke') : $t('app.telemetry_trust_grant')"
+ class="shrink-0"
+ @click="onToggleTelemetryTrust"
+ >
+ <MaterialDesignIcon
+ :icon-name="contact?.is_telemetry_trusted ? 'shield-check' : 'shield-outline'"
+ :class="contact?.is_telemetry_trusted ? 'text-blue-500' : 'text-gray-500'"
+ class="size-5"
+ />
+ </IconButton>
+ </div>
</template>
<script>
@@ -108,6 +157,10 @@ export default {
type: Boolean,
default: false,
},
+ compact: {
+ type: Boolean,
+ default: true,
+ },
},
emits: [
"conversation-deleted",
@@ -123,6 +176,7 @@ export default {
data() {
return {
contact: null,
+ GlobalState,
};
},
computed: {
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 42f07945..de293e33 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -6,23 +6,23 @@
>
<!-- banished overlay -->
<div
- v-if="GlobalState.config.banished_effect_enabled && isSelectedPeerBlocked"
+ v-if="GlobalState?.config?.banished_effect_enabled && isSelectedPeerBlocked"
class="banished-overlay"
- :style="{ background: GlobalState.config.banished_color + '33' }"
+ :style="{ background: (GlobalState?.config?.banished_color || '#dc2626') + '33' }"
>
<span
class="banished-text !opacity-100 !text-white !shadow-lg !bg-red-600 !px-4 !py-2 !rounded-xl !border-2 !tracking-widest"
:style="{
- 'background-color': GlobalState.config.banished_color,
- 'border-color': GlobalState.config.banished_color,
+ 'background-color': GlobalState?.config?.banished_color || '#dc2626',
+ 'border-color': GlobalState?.config?.banished_color || '#dc2626',
}"
- >{{ GlobalState.config.banished_text }}</span
+ >{{ GlobalState?.config?.banished_text || "BANISHED" }}</span
>
</div>
<!-- header -->
<div
- class="relative z-20 flex items-center px-4 py-3 border-b border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm"
+ class="relative z-20 flex flex-wrap items-center gap-y-2 px-3 sm:px-4 py-3 border-b border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm"
>
<!-- peer icon -->
<div class="flex-shrink-0 mr-3">
@@ -127,6 +127,7 @@
<ConversationDropDownMenu
v-if="selectedPeer"
:peer="selectedPeer"
+ :compact="compactPeerActions"
:has-failed-messages="hasFailedOrCancelledMessages"
@conversation-deleted="onConversationDeleted"
@set-custom-display-name="updateCustomDisplayName"
@@ -1563,7 +1564,7 @@
:is-sending-message="false"
:can-send-message="canSendMessage"
:delivery-method="newMessageDeliveryMethod"
- :compact="isMobile"
+ :compact="compactSendLayout"
:sending-tooltip="sendMessagePathfindingTooltip"
@send="sendMessage"
@delivery-method-changed="newMessageDeliveryMethod = $event"
@@ -2268,7 +2269,8 @@ export default {
},
myLxmfAddressHash: {
type: String,
- required: true,
+ required: false,
+ default: "",
},
selectedPeer: {
type: Object,
@@ -2363,14 +2365,22 @@ export default {
updateTimer: null,
sendStatusUiMs: Date.now(),
sendStatusTickInterval: null,
+ windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
};
},
computed: {
+ compactPeerActions() {
+ return this.windowWidth < 640;
+ },
+ compactSendLayout() {
+ return this.windowWidth < 640;
+ },
bubbleStyles() {
void GlobalState.detailedOutboundSendStatus;
void this.sendStatusUiMs;
return (chatItem) => {
const styles = {};
+ const cfg = GlobalState?.config;
const m = chatItem.lxmf_message;
const isFailed = ["cancelled", "failed"].includes(m.state);
@@ -2380,24 +2390,24 @@ export default {
styles["color"] = "#ffffff";
return styles;
}
- const color = GlobalState.config.message_failed_bubble_color || "#ef4444";
+ const color = cfg?.message_failed_bubble_color || "#ef4444";
styles["background-color"] = color;
styles["color"] = "#ffffff";
} else if (chatItem.is_outbound) {
if (chatItem.lxmf_message?._pendingPathfinding) {
if (!this.showRichOutboundPendingUi(chatItem)) {
- const color = GlobalState.config.message_outbound_bubble_color || "#4f46e5";
+ const color = cfg?.message_outbound_bubble_color || "#4f46e5";
styles["background-color"] = color;
styles["color"] = "#ffffff";
return styles;
}
return {};
}
- const color = GlobalState.config.message_outbound_bubble_color || "#4f46e5";
+ const color = cfg?.message_outbound_bubble_color || "#4f46e5";
styles["background-color"] = color;
styles["color"] = "#ffffff";
- } else if (GlobalState.config.message_inbound_bubble_color) {
- styles["background-color"] = GlobalState.config.message_inbound_bubble_color;
+ } else if (cfg?.message_inbound_bubble_color) {
+ styles["background-color"] = cfg.message_inbound_bubble_color;
}
return styles;
@@ -2701,8 +2711,16 @@ export default {
}, 2000);
this._scheduleOutboundSendStatusTick();
+
+ this._onWindowResize = () => {
+ this.windowWidth = window.innerWidth;
+ };
+ window.addEventListener("resize", this._onWindowResize);
},
beforeUnmount() {
+ if (this._onWindowResize) {
+ window.removeEventListener("resize", this._onWindowResize);
+ }
if (this.updateTimer) {
clearInterval(this.updateTimer);
}
diff --git a/meshchatx/src/frontend/components/messages/MessagesPage.vue b/meshchatx/src/frontend/components/messages/MessagesPage.vue
index 5acaf104..e084c296 100644
--- a/meshchatx/src/frontend/components/messages/MessagesPage.vue
+++ b/meshchatx/src/frontend/components/messages/MessagesPage.vue
@@ -48,7 +48,7 @@
<ConversationViewer
ref="conversation-viewer"
:config="config"
- :my-lxmf-address-hash="config?.lxmf_address_hash"
+ :my-lxmf-address-hash="config?.lxmf_address_hash || ''"
:selected-peer="selectedPeer"
:conversations="conversations"
@update:selected-peer="onPeerClick"
@@ -126,6 +126,10 @@ import WebSocketConnection from "../../js/WebSocketConnection";
import MessagesSidebar from "./MessagesSidebar.vue";
import ConversationViewer from "./ConversationViewer.vue";
import GlobalState, { mergeGlobalConfig } from "../../js/GlobalState";
+
+function snapshotGlobalConfig() {
+ return GlobalState.config && typeof GlobalState.config === "object" ? { ...GlobalState.config } : {};
+}
import DialogUtils from "../../js/DialogUtils";
import GlobalEmitter from "../../js/GlobalEmitter";
import ToastUtils from "../../js/ToastUtils";
@@ -150,7 +154,7 @@ export default {
reloadInterval: null,
conversationRefreshTimeout: null,
- config: null,
+ config: snapshotGlobalConfig(),
hasLoadedConversations: false,
peers: {},
selectedPeer: null,
@@ -283,8 +287,8 @@ export default {
this.config = next;
}
} catch (e) {
- // do nothing if failed to load config
console.log(e);
+ this.config = snapshotGlobalConfig();
}
},
async onWebsocketMessage(message) {
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────